package com.inspectortime.repository.impl;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.orm.hibernate3.SessionFactoryUtils;

import com.inspectortime.common.PersistentEntityBase;
import com.inspectortime.common.UnknownPersistentEntityException;
import com.inspectortime.domain.Employee;
import com.inspectortime.domain.Inspection;
import com.inspectortime.domain.Inspector;
import com.inspectortime.domain.InspectorCompany;
import com.inspectortime.domain.OptionalService;
import com.inspectortime.domain.RealEstateAgent;
import com.inspectortime.domain.Scheduler;
import com.inspectortime.domain.User;
import com.inspectortime.domain.ZipCodeGeoData;
import com.inspectortime.domain.billing.ConfirmedStatus;
import com.inspectortime.domain.billing.Invoice;
import com.inspectortime.domain.billing.InvoiceLineItem;
import com.inspectortime.domain.billing.Payment;
import com.inspectortime.domain.billing.PaymentLineItem;
import com.inspectortime.domain.billing.PaymentSourceType;
import com.inspectortime.domain.impl.InspectionImpl;
import com.inspectortime.domain.impl.InspectorCompanyImpl;
import com.inspectortime.domain.impl.InspectorImpl;
import com.inspectortime.domain.impl.OptionalServiceImpl;
import com.inspectortime.domain.impl.RealEstateAgentImpl;
import com.inspectortime.domain.impl.SchedulerImpl;
import com.inspectortime.domain.impl.UserImpl;
import com.inspectortime.domain.pricing.BaseFeeWithAdjustersCalculator;
import com.inspectortime.domain.pricing.FeeCalculator;
import com.inspectortime.domain.type.InspectionStatus;
import com.inspectortime.repository.Repository;
import com.mkode.util.java.lang.ClassUtils;
import com.mkode.util.java.lang.CollectionUtils;

/**
 * @author mkvalsvik
 */
public class RepositoryImpl implements Repository {

	private SessionFactory sessionFactory;

	public RepositoryImpl(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#deleteInspection(com.
	 * inspectortime.domain.Inspection)
	 */
	public void deleteInspection(Inspection inspection) {
		inspection.setInspector(null);
		inspection.setReferringAgent(null);
		inspection.getOptionalServices().clear();
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		session.delete(inspection);
	}

	public void deleteInspector(Inspector inspector) {
		deleteAllInspectionsForInspector(inspector);
		deleteEmployee(inspector);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#deleteAllInspectionsForInspector
	 * (com.inspectortime.domain.Inspector)
	 */
	public void deleteAllInspectionsForInspector(Inspector inspector) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		List<Inspection> inspections = inspector.findInspectionsBetween(null,
				null);
		for (Inspection inspection : inspections) {

			// First we need to remove all OptionalService objects because the
			// delete does not cascade do them (for some reason)
			for (int i = 0; i < inspection.getOptionalServices().size(); i++) {
				inspection.getOptionalServices().remove(i);
			}
			flush();

			// Note we're not calling deleteInspection(inspection) because that
			// method is intended to leave the associated inspector/company
			// intact
			session.delete(inspection);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#
	 * deleteAllInspectionsForInspectorCompany
	 * (com.inspectortime.domain.InspectorCompany)
	 */
	public void deleteAllInspectionsForInspectorCompany(InspectorCompany company) {
		Collection<Inspector> inspectors = company.getInspectors();
		for (Inspector inspector : inspectors) {
			deleteAllInspectionsForInspector(inspector);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#deleteEmployee(com.inspectortime
	 * .domain.Employee)
	 */
	// TODO: do we still need this? or should we just use the user.status
	// property?
	public void deleteEmployee(Employee employee) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		employee.setCompany(null);
		session.delete(employee);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#deleteInspectorCompany(com
	 * .inspectortime.domain.InspectorCompany)
	 */
	public void deleteInspectorCompany(InspectorCompany company) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);

		// Deletes of inspector won't cascade to inspections, therefore we
		// delete inspections here
		deleteAllInspectionsForInspectorCompany(company);
		session.delete(company);
	}

	/*
	 * public void deleteOptionalService(OptionalService service) { Session
	 * session = SessionFactoryUtils.getSession(this .getSessionFactory(),
	 * true); service.getCompany().getOptionalServices().remove(service);
	 * service.setCompany(null); session.delete(service); }
	 */

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#deleteFeeCalculator(com.
	 * inspectortime.domain.InspectorCompany,
	 * com.inspectortime.domain.pricing.FeeCalculator)
	 */
	public void deleteFeeCalculator(InspectorCompany company, FeeCalculator calc) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		company.getFeeCalculators().remove(calc);
		calc.setInspectorCompany(null);
		session.delete(calc);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#deleteScheduler(com.
	 * inspectortime.domain.Scheduler)
	 */
	public void deleteScheduler(Scheduler scheduler) {
		deleteEmployee(scheduler);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#deleteRealEstateAgent(com
	 * .inspectortime.domain.RealEstateAgent)
	 */
	public void deleteRealEstateAgent(RealEstateAgent realEstateAgent) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		session.delete(realEstateAgent);
		flush(); // why do we need this?
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findAllRealEstateAgents()
	 */
	public List<RealEstateAgent> findAllRealEstateAgents() {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(RealEstateAgent.class);
		List<RealEstateAgent> list = crit.list();
		return list;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findAllInspectorCompanies()
	 */
	public List<InspectorCompany> findAllInspectorCompanies() {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(InspectorCompany.class);
		List<InspectorCompany> list = crit.list();
		return list;
	}

	public List<User> findAllUsers() {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(User.class);
		List<User> list = crit.list();
		return list;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findInspectionByCode(java
	 * .lang.String)
	 */
	public Inspection findInspectionByCode(String inspectionCode) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(Inspection.class);
		crit.add(Restrictions.eq("inspectionCode", inspectionCode));
		List list = crit.list();
		return (Inspection) CollectionUtils.findOne(list);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findInspectionsByReferringAgent
	 * (com.inspectortime.domain.RealEstateAgent)
	 */
	public List<Inspection> findInspectionsByReferringAgent(
			RealEstateAgent agent) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(Inspection.class);
		crit.add(Restrictions.eq("referringAgent", agent));
		List<Inspection> list = (List<Inspection>) crit.list();
		return list;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findInspectorCompaniesByZipCode
	 * (java.util.List)
	 */
	public List findInspectorCompaniesByZipCode(List<String> zipCodes) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(InspectorCompany.class);
		crit.add(Restrictions.in("address.zipCode", zipCodes));
		return crit.list();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findInspectorCompanyByName
	 * (java.lang.String)
	 */
	public InspectorCompany findInspectorCompanyByName(String name) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(InspectorCompany.class);
		crit.add(Restrictions.eq("name", name));
		List list = crit.list();
		return (InspectorCompany) CollectionUtils.findOne(list);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findInspectorCompanyByShortName
	 * (java.lang.String)
	 */
	public InspectorCompany findInspectorCompanyByShortName(String shortName) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(InspectorCompanyImpl.class);
		crit.add(Restrictions.eq("shortName", shortName));
		List list = crit.list();
		InspectorCompanyImpl inspectorCompany = (InspectorCompanyImpl) CollectionUtils
				.findOne(list);
		return inspectorCompany;
	}

	public InvoiceLineItem findInvoiceLineItemByInspectionId(String inspectionId) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(InvoiceLineItem.class);
		crit.add(Restrictions.eq("inspectionId", inspectionId));
		List list = crit.list();
		return (InvoiceLineItem) CollectionUtils.findOne(list);
	}

	public List<Invoice> findInvoicesForCompanyId(String companyId) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(Invoice.class);
		crit.add(Restrictions.eq("companyId", companyId));
		List<Invoice> list = crit.list();
		return list;
	}

	/**
	 * @see http://www.jumpingbean.co.za/blogs/mark/hibernate_hql_inner_join_on_clause
	 */
	public List<Invoice> findUnpaidInvoicesForCompanyId(String companyId) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);

		// To get around inner join on three tables problem, we first get all invoices
		List invoices = this.findInvoicesForCompanyId(companyId);
		
		// Then we remove paid ones
		List paidInvoices = findPaidInvoicesForCompanyId(companyId);
		invoices.removeAll(paidInvoices);
		return invoices;
	}

	public Object findPaymentSource(String paymentSourceId, PaymentSourceType paymentSourceType) {
		if (paymentSourceType == PaymentSourceType.INSPECTION) {
			return this.loadInspection(paymentSourceId);
		}
		if (paymentSourceType == PaymentSourceType.INSPECTOR_TIME_INVOICE) {
			return this.loadInvoice(paymentSourceId);
		}	
		throw new RuntimeException("Unknown PaymentSourceType");
	}
	
	/**
	 * @see http://www.jumpingbean.co.za/blogs/mark/hibernate_hql_inner_join_on_clause
	 */
	public List<Invoice> findPaidInvoicesForCompanyId(String companyId) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Query query = session.createQuery("select invoice from Invoice invoice, PaymentLineItem line, Payment payment where invoice.id = line.paymentSourceId and line.payment = payment and payment.confirmedStatus = " + ConfirmedStatus.CONFIRMED.ordinal());
		List invoices = query.list();
		return invoices;
	}

	/**
	 * Note that we typically expect one payment line item per source. However,
	 * there may be multiple incomplete payments if a payment request was
	 * initiated and not finished.
	 * 
	 * @param sourceId
	 * @param type
	 * @return
	 */
	public List<PaymentLineItem> findPaymentLineItemsBySource(String sourceId) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(PaymentLineItem.class);
		crit.add(Restrictions.eq("paymentSourceId", sourceId));
		List<PaymentLineItem> list = crit.list();
		return list;
	}

	public Payment findPaymentByInvoiceNumber(
			String invoiceNumber) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(Payment.class);
		crit.add(Restrictions.eq("paymentInvoiceNumber", invoiceNumber));
		List<Payment> list = crit.list();
		if (list.size() > 2) {
			throw new RuntimeException(
					"Found multiple payments for payment invoice number "
							+ invoiceNumber
							+ ". This should not happen, check payment save code everywhere.");
		}
		return (Payment) CollectionUtils.findOne(list);
	}

	/**
	 * Returns the first completed payment. There should only be one (unless
	 * there is a developer error which has allowed the creation of two
	 * completed payments for the same source).
	 * 
	 * @param sourceId
	 * @param type
	 * @return
	 */
	public Payment findCompletedPaymentBySource(String sourceId) {
		PaymentLineItem foundPaymentLine = null;
		List<PaymentLineItem> list = findPaymentLineItemsBySource(sourceId);
		for (PaymentLineItem paymentLine : list) {
			if (paymentLine.getPayment().getConfirmedStatus() == ConfirmedStatus.CONFIRMED) {

				// Check against developer error
				if (foundPaymentLine != null) {
					throw new RuntimeException(
							"Found multiple confirmed payments for source "
									+ sourceId
									+ ". This should not happen, check payment save code everywhere.");
				} else {
					foundPaymentLine = paymentLine;
				}
			}
		}
		if (foundPaymentLine != null) {
			return foundPaymentLine.getPayment();	
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findQuickBookedInspections()
	 */
	public List<Inspection> findQuickBookedInspections() {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(Inspection.class);
		crit.add(Restrictions.eq("inspectionStatus",
				InspectionStatus.QUICK_BOOK));
		List list = crit.list();
		return list;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findRealEstateAgentByCode
	 * (java.lang.String)
	 */
	public RealEstateAgent findRealEstateAgentByCode(String code) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(RealEstateAgentImpl.class);
		crit.add(Restrictions.eq("code", code));
		List list = crit.list();
		return (RealEstateAgent) CollectionUtils.findOne(list);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findUserByEmail(java.lang
	 * .String)
	 */
	public User findUserByEmail(String email) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(User.class);
		crit.add(Restrictions.eq("email", email));

		User user = (User) CollectionUtils.findOne(crit.list());
		return validateUserStatus(user);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findUserByEmailOrUserName
	 * (java.lang.String)
	 */
	public User findUserByEmailOrUserName(String searchTerm) {
		User user = null;
		if (searchTerm.indexOf('@') > 0) {
			user = this.findUserByEmail(searchTerm);
		} else {
			user = this.findUserByUserName(searchTerm);
		}
		return user;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findUserByPasswordResetToken
	 * (java.lang.String)
	 */

	public User findUserByPasswordResetToken(String token) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(User.class);
		crit.add(Restrictions.eq("passwordResetToken", token));

		User user = (User) CollectionUtils.findOne(crit.list());
		return validateUserStatus(user);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findUserByUserName(java.
	 * lang.String)
	 */
	public User findUserByUserName(String userName) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(User.class);
		crit.add(Restrictions.eq("userName", userName));
		// crit.add(Restrictions.sqlRestriction(
		// "(this_1_.status<>2 or this_2_.status<>2)"));
		// crit.add(Restrictions.eq("status", Employee.Status.ACTIVE));

		User user = (User) CollectionUtils.findOne(crit.list());
		return validateUserStatus(user);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findZipCodeGeoData(java.
	 * lang.String)
	 */
	public ZipCodeGeoData findZipCodeGeoData(String zipCode) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(ZipCodeGeoData.class);
		crit.add(Restrictions.eq("zipCode", zipCode));
		List list = crit.list();
		return (ZipCodeGeoData) CollectionUtils.findOne(list);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#findZipCodeGeoDataInRange
	 * (double, double, double, double)
	 */
	public List<ZipCodeGeoData> findZipCodeGeoDataInRange(double lowLatitude,
			double highLatitude, double lowLongitude, double highLongitude) {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Criteria crit = session.createCriteria(ZipCodeGeoData.class);
		crit.add(Restrictions.between("latitude", Double.valueOf(lowLatitude),
				Double.valueOf(highLatitude)));
		crit.add(Restrictions.between("longitude",
				Double.valueOf(lowLongitude), Double.valueOf(highLongitude)));
		List<ZipCodeGeoData> list = crit.list();
		return list;
	}

	/**
	 * TODO: Move flush to the end of OpenSessionInView to speed things up and
	 * reduce number of flushes
	 */
	public void flush() {
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		session.flush();
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#
	 * loadBaseFeeWithAdjustersCalculator(java.io.Serializable)
	 */
	public BaseFeeWithAdjustersCalculator loadBaseFeeWithAdjustersCalculator(
			Serializable id) {
		return (BaseFeeWithAdjustersCalculator) loadPersistentEntity(
				BaseFeeWithAdjustersCalculator.class, id);
	}

	public Inspection loadInspection(Serializable id) {
		return (Inspection) loadPersistentEntity(InspectionImpl.class, id);
	}

	public Payment loadPayment(Serializable id) {
		return (Payment) loadPersistentEntity(Payment.class, id);
	}

	public Invoice loadInvoice(Serializable id) {
		return (Invoice) loadPersistentEntity(Invoice.class, id);
	}

	/**
	 * Loads an object and wraps objects not found in a local exception in order
	 * to reduce the tight coupling with Hibernate
	 * 
	 * @param clazz
	 *            - the class of the desired object
	 * @param id
	 *            - the identifier of desired object
	 * @throws com.inspectortime.repository.ObjectNotFoundException
	 *             if object not found
	 */
	private Object loadPersistentEntity(Class clazz, Serializable id) {
		if (!ClassUtils.isInstanceOf(clazz, PersistentEntityBase.class)) {
			throw new UnknownPersistentEntityException(
					"Class does not inherit from persistent entity base class: "
							+ clazz.getName());
		}
		Session session = SessionFactoryUtils.getSession(this
				.getSessionFactory(), true);
		Object obj = null;
		try {
			obj = session.load(clazz, id);
		} catch (org.hibernate.ObjectNotFoundException e) {
			throw new com.inspectortime.repository.ObjectNotFoundException(e
					.getMessage(), e);
		}
		return obj;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#loadInspector(java.io.
	 * Serializable)
	 */
	public Inspector loadInspector(Serializable id) {
		Inspector inspector = (Inspector) loadPersistentEntity(
				InspectorImpl.class, id);
		return inspector;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#loadInspectorCompany(java
	 * .io.Serializable)
	 */
	public InspectorCompany loadInspectorCompany(Serializable id) {
		return (InspectorCompany) loadPersistentEntity(
				InspectorCompanyImpl.class, id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#loadOptionalService(java
	 * .io.Serializable)
	 */
	public OptionalService loadOptionalService(Serializable id) {
		return (OptionalService) loadPersistentEntity(
				OptionalServiceImpl.class, id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#loadRealEstateAgent(java
	 * .io.Serializable)
	 */
	public RealEstateAgent loadRealEstateAgent(Serializable id) {
		return (RealEstateAgent) loadPersistentEntity(
				RealEstateAgentImpl.class, id);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.inspectortime.repository.impl.Repository#loadScheduler(java.io.
	 * Serializable)
	 */
	public Scheduler loadScheduler(Serializable id) {
		Scheduler scheduler = (Scheduler) loadPersistentEntity(
				SchedulerImpl.class, id);
		return scheduler;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.inspectortime.repository.impl.Repository#loadUser(java.io.Serializable
	 * )
	 */
	public User loadUser(Serializable id) {
		User user = (User) loadPersistentEntity(UserImpl.class, id);
		return user;
	}

	private User validateUserStatus(User user) {
		if (user instanceof Employee) {
			Employee.Status status = ((Employee) user).getStatus();
			if (status != Employee.Status.DELETED) {
				return user;
			}
			return null;
		}
		return user;
	}

}
